package us.gaaoc.framework.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.hibernate.SessionFactory;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.ConnectionProxy;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * A DataSource that provides Connection sharing between Hibernate and other
 * Connection consumers. Connection sharing is important when you need to
 * perform data access tasks with both hibernate and another mechanism in the
 * same transaction, without isolating them from each other.
 * 
 * Normally, when Hibernate and JDBC data access in the same transaction each
 * will have its own database connection. So even they are in the same
 * application transaction, they are performed in different database
 * transactions. Therefore one will not see updates made by the other until it
 * is committed.
 * 
 * When a consumer asks for a Connection, we first check if hibernate already
 * opened one for this thread. If it hasn't we simply delegate the request to
 * the underlying DataSource. If it has, we return hibernate's Connection.
 * 
 * Do not use this DataSource with a hibernate SessionFactory! The
 * SessionFactory should access the underlying DataSource directly.
 * 
 * @author assaf
 */
public class HibernateConnectionDataSource extends AbstractDataSource {

	// private static final Logger logger = Logger
	// .getLogger(HibernateConnectionDataSource.class);

	private DataSource targetDataSource;
	private SessionFactory sessionFactory;

	public HibernateConnectionDataSource(SessionFactory sessionFactory,DataSource dataSource) {
		this.sessionFactory = sessionFactory;
		this.targetDataSource = dataSource;
	}
	
	/**
	 * @return Returns the targetDataSource.
	 */
	public DataSource getTargetDataSource() {
		return targetDataSource;
	}

	/**
	 * @param targetDataSource
	 *            The targetDataSource to set.
	 */
	public void setTargetDataSource(DataSource targetDataSource) {
		this.targetDataSource = targetDataSource;
	}

	/**
	 * @return Returns the sessionFactory.
	 */
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * @param sessionFactory
	 *            The sessionFactory to set.
	 */
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * 
	 * @return Connection
	 * @throws SQLException
	 * @see javax.sql.DataSource#getConnection()
	 */
	public Connection getConnection() throws SQLException {
		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.getResource(sessionFactory);
		if (sessionHolder == null) {
			// logger.debug("Didn't find a Session bound to the current thread, getting Connection from DataSource");
			return targetDataSource.getConnection();
		}

		/*
		 * return a close-suppressing connection wrapper around the hibernate
		 * managed connection
		 */
		// logger.debug("Found a Session bound to the current thread, reusing its connection");
		Connection hibernateConnection = sessionHolder.getSession()
				.connection();
		return (Connection) Proxy.newProxyInstance(ConnectionProxy.class
				.getClassLoader(), new Class[] { ConnectionProxy.class },
				new TransactionAwareInvocationHandler(hibernateConnection,
						targetDataSource));
	}

	/**
	 * 
	 * @param username
	 * @param password
	 * @return Connection
	 * @throws SQLException
	 * @see javax.sql.DataSource#getConnection(java.lang.Strin g,
	 *      java.lang.String)
	 */
	public Connection getConnection(String username, String password)
			throws SQLException {

		throw new UnsupportedOperationException("use getConnection() instead");
	}

	/**
	 * Invocation handler that delegates close calls on JDBC Connections to
	 * DataSourceUtils for being aware of thread-bound transactions.
	 * 
	 * (based on TransactionAwareDataSourceProxy)
	 */
	private static class TransactionAwareInvocationHandler implements
			InvocationHandler {

		private final Connection target;

		private final DataSource dataSource;

		public TransactionAwareInvocationHandler(Connection target,
				DataSource dataSource) {
			this.target = target;
			this.dataSource = dataSource;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			// Invocation on ConnectionProxy interface coming in...

			if (method.getName().equals("getTargetConnection")) {
				// Handle getTargetConnection method: return underlying
				// Connection.
				return this.target;
			} else if (method.getName().equals("equals")) {
				// Only consider equal when proxies are identical.
				return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
			} else if (method.getName().equals("hashCode")) {
				// Use hashCode of Connection proxy.
				return new Integer(hashCode());
			} else if (method.getName().equals("close")) {
				// never close the connection, it is managed by hibernate!
				// logger.debug("Supressing connection close because it is managed externally");
				return null;
			}

			// Invoke method on target Connection.
			try {
				Object retVal = method.invoke(this.target, args);

				// If return value is a Statement, apply transaction timeout.
				// Applies to createStatement, prepareStatement, prepareCall.
				if (retVal instanceof Statement) {
					DataSourceUtils.applyTransactionTimeout((Statement) retVal,
							this.dataSource);
				}

				return retVal;
			} catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}
	}

	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
		// TODO Auto-generated method stub
		return null;
	}

}